﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Threading.Tasks;
using RadaCode.SwissKnife.Random;

namespace RadaCode.SwissKnife
{
    public class KMeansForPoints
    {
        public static List<Point> KMeansGetCentroids(List<Point> samples, List<Point> prevCenters, int? targetNumerOfClusters)
        {
            var finalCentroids = new List<Point>();

            if(prevCenters == null && targetNumerOfClusters == null)
            {
                throw new ArgumentNullException("If you don't specify prev centers you have to tell me how many clusters you desire to get");
            }
            else if (prevCenters == null)
            {
                prevCenters = PickRandomCenters(new MersenneTwister(), (int) targetNumerOfClusters, samples);
            }

            Dictionary<Point, List<Point>> centerAssignments = ParallelGetCenterAssignments(samples, prevCenters);

            List<Point> oldCenters = null;
            while (true)
            {
                //calculate average center
                List<Point> newCenters = ParallelGetNewCenters(centerAssignments);

                if (CentersEqual(newCenters, oldCenters))
                {
                    finalCentroids = newCenters;
                    break;
                }

                centerAssignments = ParallelGetCenterAssignments(samples, newCenters);

                oldCenters = newCenters;
            }

            return finalCentroids;
        }

        private static Dictionary<Point, List<Point>> ParallelGetCenterAssignments(List<Point> points, List<Point> centers)
        {
            var centerAssignments = new Dictionary<Point, List<Point>>();

            foreach (Point point in centers)
            {
                if(!centerAssignments.ContainsKey(point))
                centerAssignments.Add(point, new List<Point>());
            }

            //Parallel.ForEach(points, point =>
            points.AsParallel().ForAll(point =>
            {
                double x = point.X;
                double y = point.Y;

                Point closestCenter = new Point();
                double closestCenterDistance = double.MaxValue;

                foreach (Point pickedPoint in centers)
                {
                    double centerX = pickedPoint.X;
                    double centerY = pickedPoint.Y;

                    double distance = Math.Sqrt(Math.Pow(x - centerX, 2)
                                        + Math.Pow(y - centerY, 2));

                    if (distance < closestCenterDistance)
                    {
                        closestCenterDistance = distance;
                        closestCenter = pickedPoint;
                    }
                }

                lock (centerAssignments)
                {
                    centerAssignments[closestCenter].Add(point);
                }
            });

            return centerAssignments;
        }

        private static List<Point> ParallelGetNewCenters(Dictionary<Point, List<Point>> centerAssignments)
        {
            var newCenters = new Point[centerAssignments.Keys.Count];

            Parallel.ForEach(centerAssignments.Keys, (center, state, i) =>
            {
                double totalX = 0;
                double totalY = 0;

                foreach (Point point in centerAssignments[center])
                {
                    totalX += point.X;
                    totalY += point.Y;
                }

                double averageX = totalX / centerAssignments[center].Count;
                double averageY = totalY / centerAssignments[center].Count;

                var newCenter = new Point((int)averageX, (int)averageY);
                newCenters[i] = newCenter;
            });

            return newCenters.ToList();
        }

        private static bool CentersEqual(List<Point> newCenters, List<Point> oldCenters)
        {
            if (newCenters == null || oldCenters == null)
            {
                return false;
            }

            foreach (Point newCenter in newCenters)
            {
                bool found = false;
                foreach (Point oldCenter in oldCenters)
                {
                    if (newCenter.X == oldCenter.X && newCenter.Y == oldCenter.Y)
                    {
                        found = true;
                        break;
                    }
                }

                if (!found)
                {
                    return false;
                }
            }
            return true;
        }

        private static List<Point> PickRandomCenters(MersenneTwister random, int clusters, List<Point> points)
        {
            if (clusters == points.Count)
            {
                return points;
            }
            var randomCenters = new List<Point>();
            int pickedPointCount = 0;
            while (pickedPointCount < clusters)
            {
                var point = points[random.Next(0, points.Count - 1)];
                if (!randomCenters.Contains(point))
                {
                    randomCenters.Add(point);
                    pickedPointCount++;
                }
            }
            return randomCenters;
        }
    }

}
